﻿/*
 * MetaInfo
 * 
 *  Parse structured date to meaningful data
 *  Based on The BitTorrent Protocol Specification v11031
 *  see http://www.bittorrent.org/beps/bep_0003.html
 * 
 *  Attention:
 *      creation date is the total seconds from 1970.1.1 00:00:00 to now in UTC
 * 
 * MetaFileInfo
 * 
 * PeerInfo
 * 
 * PeerID
 * 
 *  peer id generate algorithm is based on Peer ID Conventions Protocol v11031
 *  see http://www.bittorrent.org/beps/bep_0020.html
 * 
 * 
 * 
 * Author: snowdreamist
 * Date: 2011/11/18
 * 
 */

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BitTorrentSharp.BEncoding;

namespace BitTorrentSharp.Torrent
{
    public class MetaInfo
    {
        #region consts
        /*
         * key definition
         */
        internal const string AnnounceKey = "announce";
        internal const string CreateByKey = "created by";
        internal const string CreationDateKey = "creation date";
        internal const string EncodingKey = "encoding";
        internal const string InfoKey = "info";
        internal const string NodesKey = "nodes";
        /*
         * info
         */
        internal const string InfoFilesKey = "files";
        internal const string InfoNameKey = "name";
        internal const string InfoLengthKey = "length";
        internal const string InfoPieceLengthKey = "piece length";
        internal const string InfoPiecesKey = "pieces";
        #endregion

        #region static

        private static System.Text.Encoding _defaultEncoding = System.Text.Encoding.UTF8;
        /// <summary>
        /// Default encoding
        /// </summary>
        public static System.Text.Encoding DefaultEncoding
        {
            get
            {
                return _defaultEncoding;
            }
            set
            {
                if (value != null)
                    _defaultEncoding = value;
            }
        }

        private static DateTime _basicTime = new DateTime(1970, 1, 1, 0, 0, 0);
        /// <summary>
        /// Basic time used to calculate creation datetime
        /// </summary>
        public static DateTime BasicTime
        {
            get
            {
                return _basicTime;
            }
        }
        /// <summary>
        /// create metainfo from stream
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public static MetaInfo CreateFromStream(Stream stream)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");

            var rootObject = BValue.Deserialize(stream);
            if (!(rootObject is BDictionary))
                throw new ArgumentException("Invalid root object type");

            return new MetaInfo(rootObject as BDictionary);
        }
        #endregion

        public MetaInfo(BDictionary orginalInformation)
        {
            this.OrginalInformation = orginalInformation;
            this.ParseOrginalInformation();
        }

        #region MetaInfo
        /// <summary>
        /// announce
        /// </summary>
        public List<string> Announces { get; private set; }
        /// <summary>
        /// create by
        /// </summary>
        public string CreateBy { get; private set; }
        /// <summary>
        /// create datetime
        /// </summary>
        public DateTime CreationDate { get; private set; }
        /// <summary>
        /// the encoding of this file
        /// </summary>
        public System.Text.Encoding Encoding { get; private set; }
        /// <summary>
        /// encoding text
        /// </summary>
        public string EncodingText { get; private set; }
        /// <summary>
        /// name
        /// </summary>
        public string Name { get; private set; }
        /// <summary>
        /// files
        /// ATTENTION:
        ///     different from original bit torrent definition, in both single file and multiple files situation, 
        ///     Files will be filled with meta file info
        /// </summary>
        public List<MetaFileInfo> Files { get; private set; }
        /// <summary>
        /// length of file(s)
        /// ATTENTION:
        ///     different from original bit torrent definition, in both single file and multiple files situation,
        ///     Length will be assigned single file length or total files length
        /// </summary>
        public long Length { get; private set; }
        /// <summary>
        /// piece length
        /// </summary>
        public long PieceLength { get; private set; }
        /// <summary>
        /// piece SHA hash codes
        /// </summary>
        public List<byte[]> PieceSHAHashCodes { get; private set; }
        #endregion

        #region internal
        /// <summary>
        /// original information, internal only
        /// </summary>
        internal BDictionary OrginalInformation { get; private set; }
        #endregion

        #region private

        private void ParseOrginalInformation()
        {
            // announce [Required]
            if (!this.OrginalInformation.ContainsKey(MetaInfo.AnnounceKey))
                throw new ArgumentException("announce not found");

            var announce = this.OrginalInformation[MetaInfo.AnnounceKey];

            if (announce is BString)
                this.Announces = new List<string>() { (BString)announce };
            else if (announce is BList)
                this.Announces = ((BList)announce).Select(p => (string)(BString)p).ToList();
            else
                throw new ArgumentException("Invalid announce type");

            // create by [Optional]
            if (this.OrginalInformation.ContainsKey(MetaInfo.CreateByKey))
                this.CreateBy = (BString)this.OrginalInformation[MetaInfo.CreateByKey];

            // creation date [Optional]
            if (this.OrginalInformation.ContainsKey(MetaInfo.CreationDateKey))
                this.CreationDate = TimeZone.CurrentTimeZone.ToLocalTime(MetaInfo.BasicTime.AddSeconds(((BNumber)this.OrginalInformation[MetaInfo.CreationDateKey])));

            // encoding [Optional]
            if (this.OrginalInformation.ContainsKey(MetaInfo.EncodingKey))
            {
                this.EncodingText = (BString)this.OrginalInformation[MetaInfo.EncodingKey];
                if (!string.IsNullOrWhiteSpace(this.EncodingText))
                    this.Encoding = System.Text.Encoding.GetEncoding(this.EncodingText);
            }

            // info [Required]
            if (!this.OrginalInformation.ContainsKey(MetaInfo.InfoKey))
                throw new ArgumentException("info not found");
            // parse info
            this.ParseInfo((BDictionary)this.OrginalInformation[MetaInfo.InfoKey]);

            // nodes [Optional]
            if (this.OrginalInformation.ContainsKey(MetaInfo.NodesKey))
                this.ParseNodes((BList)this.OrginalInformation[MetaInfo.NodesKey]);
        }

        private void ParseInfo(BDictionary infoDict)
        {
            if (infoDict == null)
                throw new ArgumentNullException("infoDict");

            // verify
            // files and length cannot both or neither exist
            if (infoDict.ContainsKey(MetaInfo.InfoFilesKey) && infoDict.ContainsKey(MetaInfo.InfoLengthKey))
                throw new ArgumentException("files and length cannot both exists");
            if (!infoDict.ContainsKey(MetaInfo.InfoFilesKey) && !infoDict.ContainsKey(MetaInfo.InfoLengthKey))
                throw new ArgumentException("files and length cannot neither exists");

            // name [Required]
            bool nameParsed = false;

            if (!string.IsNullOrWhiteSpace(this.EncodingText) && this.Encoding != null)
            {
                string encodedNameKey = MetaInfo.InfoNameKey + '.' + this.EncodingText;
                if (infoDict.ContainsKey(encodedNameKey))
                {
                    this.Name = ((BString)infoDict[encodedNameKey]).ToString(this.Encoding);
                    nameParsed = true;
                }
            }

            if (!nameParsed && infoDict.ContainsKey(MetaInfo.InfoNameKey))
            {
                // use standard name key
                this.Name = ((BString)infoDict[MetaInfo.InfoNameKey]).ToString(this.Encoding != null ? this.Encoding : MetaInfo.DefaultEncoding);
                nameParsed = true;
            }

            // files and length [Optional]
            if (infoDict.ContainsKey(MetaInfo.InfoFilesKey))
            {
                this.ParseFiles((BList)infoDict[MetaInfo.InfoFilesKey]);
                // calculate total length
                this.Length = this.Files.Sum(p => p.Length);
            }
            else
            {
                this.Length = (BNumber)infoDict[MetaInfo.InfoLengthKey];
                // create meta file info
                this.Files = new List<MetaFileInfo>()
                {
                    new MetaFileInfo(
                        new BDictionary()
                        {
                            {MetaFileInfo.PathKey, new BList(){ (BString)this.Name }},
                            {MetaFileInfo.LengthKey, (BNumber)this.Length}
                        },
                        this
                        )
                };
            }

            // piece length [Required]
            if (!infoDict.ContainsKey(MetaInfo.InfoPieceLengthKey))
                throw new ArgumentException("piece length not found");
            this.PieceLength = (BNumber)infoDict[MetaInfo.InfoPieceLengthKey];

            // pieces [Required]
            if (!infoDict.ContainsKey(MetaInfo.InfoPiecesKey))
                throw new ArgumentException("pieces not found");
            this.ParsePieces((BString)infoDict[MetaInfo.InfoPiecesKey]);
        }

        private void ParseNodes(BList nodeLst)
        {
            if (nodeLst == null)
                throw new ArgumentNullException("nodeLst");
        }
        
        private void ParseFiles(BList fileLst)
        {
            if (fileLst == null)
                throw new ArgumentNullException("fileLst");
            if (fileLst.Count == 0)
                throw new ArgumentException("file list is zero length");

            this.Files = new List<MetaFileInfo>();

            foreach (var file in fileLst)
            {
                var fileInfoDict = (BDictionary)file;
                if (fileInfoDict == null)
                    throw new ArgumentException("Invalid file info type");
                // create meta file info
                MetaFileInfo metaFileInfo = new MetaFileInfo((BDictionary)fileInfoDict, this);
                this.Files.Add(metaFileInfo);
            }
        }

        private void ParsePieces(byte[] pieces)
        {
            // verify
            if (pieces.Length % 20 != 0)
                throw new ArgumentException(string.Format("Invalid pieces length {0}", pieces.Length));
            
            this.PieceSHAHashCodes = new List<byte[]>();

            int count = pieces.Length / 20;
            for(int i = 0; i < count; ++i)
                this.PieceSHAHashCodes.Add(pieces.Skip(i * 20).Take(20).ToArray());
        }
        #endregion
    }

    /// <summary>
    /// meta file info
    /// </summary>
    public class MetaFileInfo
    {
        #region consts
        /*
         * info files
         */
        internal const string PathKey = "path";
        internal const string LengthKey = "length";
        internal const string FileHashKey = "filehash";
        internal const string ED2KKey = "ed2k";
        #endregion

        public MetaFileInfo(BDictionary fileInfoDict, MetaInfo metaInfo)
        {
            if (fileInfoDict == null)
                throw new ArgumentNullException("fileInfoDict");
            if (metaInfo == null)
                throw new ArgumentNullException("metaInfo");
            this._metaInfo = metaInfo;
            this.OrginialFileInfo = fileInfoDict;
            this.ParseFileInfo();
        }
        /// <summary>
        /// path
        /// </summary>
        public string Path { get; private set; }
        /// <summary>
        /// length
        /// </summary>
        public long Length { get; private set; }
        /// <summary>
        /// ed2k
        /// </summary>
        public byte[] ED2k { get; private set; }
        /// <summary>
        /// hash code
        /// </summary>
        public byte[] HashCode { get; private set; }

        #region Internal
        /// <summary>
        /// file info dictionary
        /// </summary>
        internal BDictionary OrginialFileInfo { get; private set; }
        #endregion

        #region private
        /// <summary>
        /// metainfo belong to
        /// </summary>
        private MetaInfo _metaInfo;
        /// <summary>
        /// parse file info
        /// </summary>
        private void ParseFileInfo()
        {
            // path [Required]
            string pathKey = null;

            // try encoded key first
            if(!string.IsNullOrWhiteSpace(this._metaInfo.EncodingText))
            {
                string encodedKey = MetaFileInfo.PathKey + '.' + this._metaInfo.EncodingText;
                if (this.OrginialFileInfo.ContainsKey(encodedKey))
                    pathKey = encodedKey;
            }

            if (pathKey == null)
            {
                if (!this.OrginialFileInfo.ContainsKey(MetaFileInfo.PathKey))
                    throw new ArgumentException("path not found");
                pathKey = MetaFileInfo.PathKey;
            }

            // combine path
            this.Path = System.IO.Path.Combine(
                ((BList)this.OrginialFileInfo[pathKey])
                .Select(p =>
                    this._metaInfo.Encoding != null
                    ?
                    this._metaInfo.Encoding.GetString(System.Text.Encoding.ASCII.GetBytes((BString)p))
                    :
                    MetaInfo.DefaultEncoding.GetString(System.Text.Encoding.ASCII.GetBytes((BString)p))
                    )
                .ToArray()
                );

            // Length [Required]
            if (!this.OrginialFileInfo.ContainsKey(MetaFileInfo.LengthKey))
                throw new ArgumentException("length not found");
            this.Length = (BNumber)this.OrginialFileInfo[MetaFileInfo.LengthKey];

            // ED2K [Optional]
            if (this.OrginialFileInfo.ContainsKey(MetaFileInfo.ED2KKey))
                this.ED2k = System.Text.Encoding.ASCII.GetBytes((BString)this.OrginialFileInfo[MetaFileInfo.ED2KKey]);

            // HashCode [Optional][Strong Recommanded]
            if (this.OrginialFileInfo.ContainsKey(MetaFileInfo.FileHashKey))
                this.HashCode = System.Text.Encoding.ASCII.GetBytes((BString)this.OrginialFileInfo[MetaFileInfo.FileHashKey]);
        }
        #endregion
    }

    /// <summary>
    /// peer info
    /// </summary>
    public class PeerInfo
    {
        #region consts
        /*
         * peer keys
         */
        internal const string PeerIDKey = "peer id";
        internal const string IPKey = "ip";
        internal const string PortKey = "port";

        #endregion

        

        public PeerInfo() { }

        public PeerInfo(Dictionary<string,object> peerInfoDict)
        {
            if (peerInfoDict == null)
                throw new ArgumentNullException("peerInfoDict");
            this.ParsePeerInfoDict(peerInfoDict);
        }

        public PeerID ID { get; set; }

        public string IPOrDNS { get; set; }

        public int Port { get; set; }

        #region private

        private void ParsePeerInfoDict(Dictionary<string,object> peerInfoDict)
        {
            if (!peerInfoDict.ContainsKey(PeerInfo.PeerIDKey))
                throw new ArgumentException("Peer id not found");

            this.ID = new PeerID(peerInfoDict[PeerInfo.PeerIDKey] as string);

            if (!peerInfoDict.ContainsKey(PeerInfo.IPKey))
                throw new ArgumentException("IP not found");

            this.IPOrDNS = peerInfoDict[PeerInfo.IPKey] as string;

            if (!peerInfoDict.ContainsKey(PeerInfo.PortKey))
                throw new ArgumentException("Port not found");

            this.Port = (int)(long)peerInfoDict[PeerInfo.PortKey];
        }

        #endregion
    }

    public enum PeerIDEncodingStyle
    {
        DashFollowsByClientType
    }

    public class PeerID
    {

        #region static

        public static PeerID GeneratePeerID(PeerIDEncodingStyle style,string clientType, Version version)
        {
            return null;
        }

        #endregion

        public PeerID(string id)
        {
            if (id == null)
                throw new ArgumentNullException("id");
            if (id.Length != 20)
                throw new ArgumentException("Peer id should be a string with length 20");
            this._peerID = id;
        }

        public override string ToString()
        {
            return this._peerID;
        }

        #region private

        private string _peerID;

        #endregion
    }
}
